﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.IO;
using System.Text;
#if SILVERLIGHT
#else
#endif

namespace fastJSON
{
    internal class JSONSerializer
    {
        private readonly StringBuilder _output = new StringBuilder();
        private readonly bool useMinimalDataSetSchema;
        private readonly bool fastguid = true;
        private readonly bool useExtension = true;
        private readonly bool serializeNulls = true;
        private readonly int _MAX_DEPTH = 10;
        private readonly bool _Indent;
        private int _current_depth;

        internal JSONSerializer(bool UseMinimalDataSetSchema, bool UseFastGuid, bool UseExtensions, bool SerializeNulls, bool IndentOutput)
        {
            this.useMinimalDataSetSchema = UseMinimalDataSetSchema;
            this.fastguid = UseFastGuid;
            this.useExtension = UseExtensions;
            this._Indent = IndentOutput;
            this.serializeNulls = SerializeNulls;
        }

        internal string ConvertToJSON(object obj)
        {
            WriteValue(obj);
            return this._output.ToString();
        }

        private void WriteValue(object obj)
        {
            if (obj == null || obj is DBNull)
            {
                this._output.Append("null");
            }
            else if (obj is string || obj is char)
            {
                WriteString((string) obj);
            }
            else if (obj is Guid)
            {
                WriteGuid((Guid) obj);
            }
            else if (obj is bool)
            {
                this._output.Append(((bool) obj) ? "true" : "false"); // conform to standard
            }
            else if (
                obj is int || obj is long || obj is double ||
                obj is decimal || obj is float ||
                obj is byte || obj is short ||
                obj is sbyte || obj is ushort ||
                obj is uint || obj is ulong
                )
            {
                this._output.Append(((IConvertible) obj).ToString(NumberFormatInfo.InvariantInfo));
            }
            else if (obj is DateTime)
            {
                WriteDateTime((DateTime) obj);
            }
            else if (obj is IDictionary<string, string>)
            {
                WriteStringDictionary((IDictionary) obj);
            }
            else if (obj is IDictionary)
            {
                WriteDictionary((IDictionary) obj);
            }
#if !SILVERLIGHT
            else if (obj is DataSet)
            {
                WriteDataset((DataSet) obj);
            }
            else if (obj is DataTable)
            {
                WriteDataTable((DataTable) obj);
            }
#endif
            else if (obj is byte[])
            {
                WriteBytes((byte[]) obj);
            }
            else if (obj is Array || obj is IList || obj is ICollection)
            {
                WriteArray((IEnumerable) obj);
            }
            else if (obj is Enum)
            {
                WriteEnum((Enum) obj);
            }

#if CUSTOMTYPE
            else if (JSON.Instance.IsTypeRegistered(obj.GetType()))
                WriteCustom(obj);
#endif
            else
            {
                WriteObject(obj);
            }
        }

#if CUSTOMTYPE
        private void WriteCustom(object obj)
        {
            Serialize s;
            JSON.Instance._customSerializer.TryGetValue(obj.GetType(), out s);
            WriteStringFast(s(obj));
        }
#endif

        private void WriteEnum(Enum e)
        {
            // TODO : optimize enum write
            WriteStringFast(e.ToString());
        }

        private void WriteGuid(Guid g)
        {
            if (this.fastguid == false)
            {
                WriteStringFast(g.ToString());
            }
            else
            {
                WriteBytes(g.ToByteArray());
            }
        }

        private void WriteBytes(byte[] bytes)
        {
#if !SILVERLIGHT
            WriteStringFast(Convert.ToBase64String(bytes, 0, bytes.Length, Base64FormattingOptions.None));
#else
            WriteStringFast(Convert.ToBase64String(bytes, 0, bytes.Length));
#endif
        }

        private void WriteDateTime(DateTime dateTime)
        {
            // datetime format standard : yyyy-MM-dd HH:mm:ss
            this._output.Append("\"");
            this._output.Append(dateTime.Year.ToString("0000", NumberFormatInfo.InvariantInfo));
            this._output.Append("-");
            this._output.Append(dateTime.Month.ToString("00", NumberFormatInfo.InvariantInfo));
            this._output.Append("-");
            this._output.Append(dateTime.Day.ToString("00", NumberFormatInfo.InvariantInfo));
            this._output.Append(" ");
            this._output.Append(dateTime.Hour.ToString("00", NumberFormatInfo.InvariantInfo));
            this._output.Append(":");
            this._output.Append(dateTime.Minute.ToString("00", NumberFormatInfo.InvariantInfo));
            this._output.Append(":");
            this._output.Append(dateTime.Second.ToString("00", NumberFormatInfo.InvariantInfo));
            this._output.Append("\"");
        }
#if !SILVERLIGHT
        private DatasetSchema GetSchema(DataSet ds)
        {
            if (ds == null)
            {
                return null;
            }
            var m = new DatasetSchema();
            m.Info = new List<string>();
            m.Name = ds.DataSetName;
            foreach (DataTable t in ds.Tables)
            {
                foreach (DataColumn c in t.Columns)
                {
                    m.Info.Add(t.TableName);
                    m.Info.Add(c.ColumnName);
                    m.Info.Add(c.DataType.ToString());
                }
            }
            // TODO : serialize relations and constraints here
            return m;
        }

        private string GetXmlSchema(DataTable dt)
        {
            using (var writer = new StringWriter())
            {
                dt.WriteXmlSchema(writer);
                return dt.ToString();
            }
        }

        private void WriteDataset(DataSet ds)
        {
            this._output.Append('{');
            if (this.useExtension)
            {
                WritePair("$schema", this.useMinimalDataSetSchema ? (object) GetSchema(ds) : ds.GetXmlSchema());
                this._output.Append(',');
            }
            foreach (DataTable table in ds.Tables)
            {
                WriteDataTableData(table);
            }
            // end dataset
            this._output.Append('}');
        }

        private void WriteDataTableData(DataTable table)
        {
            this._output.Append('\"');
            this._output.Append(table.TableName);
            this._output.Append("\":[");
            DataColumnCollection cols = table.Columns;
            foreach (DataRow row in table.Rows)
            {
                this._output.Append('[');
                bool pendingSeperator = false;
                foreach (DataColumn column in cols)
                {
                    if (pendingSeperator)
                    {
                        this._output.Append(',');
                    }
                    WriteValue(row[column]);
                    pendingSeperator = true;
                }
                this._output.Append(']');
            }
            this._output.Append(']');
        }

        private void WriteDataTable(DataTable dt)
        {
            this._output.Append('{');
            if (this.useExtension)
            {
                WritePair("$schema", this.useMinimalDataSetSchema ? (object) GetSchema(dt.DataSet) : GetXmlSchema(dt));
                this._output.Append(',');
            }
            WriteDataTableData(dt);
            // end datatable
            this._output.Append('}');
        }
#endif
        private void WriteObject(object obj)
        {
            Indent();
            this._current_depth++;
            if (this._current_depth > this._MAX_DEPTH)
            {
                throw new Exception("Serializer encountered maximum depth of " + this._MAX_DEPTH);
            }
            this._output.Append('{');
            var map = new Dictionary<string, string>();
            Type t = obj.GetType();
            bool append = false;
            if (this.useExtension)
            {
                WritePairFast("$type", JSON.Instance.GetTypeAssemblyName(t));
                append = true;
            }
            List<Getters> g = JSON.Instance.GetGetters(t);
            foreach (Getters p in g)
            {
                if (append)
                {
                    this._output.Append(',');
                }
                object o = p.Getter(obj);
                if ((o == null || o is DBNull) && this.serializeNulls == false)
                {
                    append = false;
                }
                else
                {
                    WritePair(p.Name, o);
                    if (o != null && this.useExtension)
                    {
                        Type tt = o.GetType();
                        if (tt == typeof (Object))
                        {
                            map.Add(p.Name, tt.ToString());
                        }
                    }
                    append = true;
                }
            }
            if (map.Count > 0 && this.useExtension)
            {
                this._output.Append(",\"$map\":");
                WriteStringDictionary(map);
            }
            this._current_depth--;
            Indent();
            this._output.Append('}');
            this._current_depth--;
        }

        private void Indent()
        {
            Indent(false);
        }

        private void Indent(bool dec)
        {
            if (this._Indent)
            {
                this._output.Append("\r\n");
                for (int i = 0; i < this._current_depth - (dec ? 1 : 0); i++)
                {
                    this._output.Append("\t");
                }
            }
        }

        private void WritePairFast(string name, string value)
        {
            if ((value == null) && this.serializeNulls == false)
            {
                return;
            }
            Indent();
            WriteStringFast(name);
            this._output.Append(':');
            WriteStringFast(value);
        }

        private void WritePair(string name, object value)
        {
            if ((value == null || value is DBNull) && this.serializeNulls == false)
            {
                return;
            }
            Indent();
            WriteStringFast(name);
            this._output.Append(':');
            WriteValue(value);
        }

        private void WriteArray(IEnumerable array)
        {
            Indent();
            this._output.Append('[');
            bool pendingSeperator = false;
            foreach (object obj in array)
            {
                if (pendingSeperator)
                {
                    this._output.Append(',');
                }
                WriteValue(obj);
                pendingSeperator = true;
            }
            Indent();
            this._output.Append(']');
        }

        private void WriteStringDictionary(IDictionary dic)
        {
            Indent();
            this._output.Append('{');
            bool pendingSeparator = false;
            foreach (DictionaryEntry entry in dic)
            {
                if (pendingSeparator)
                {
                    this._output.Append(',');
                }
                WritePairFast((string) entry.Key, (string) entry.Value);
                pendingSeparator = true;
            }
            Indent();
            this._output.Append('}');
        }

        private void WriteDictionary(IDictionary dic)
        {
            Indent();
            this._output.Append('[');
            bool pendingSeparator = false;
            foreach (DictionaryEntry entry in dic)
            {
                if (pendingSeparator)
                {
                    this._output.Append(',');
                }
                Indent();
                this._output.Append('{');
                WritePair("k", entry.Key);
                this._output.Append(",");
                WritePair("v", entry.Value);
                Indent();
                this._output.Append('}');
                pendingSeparator = true;
            }
            Indent();
            this._output.Append(']');
        }

        private void WriteStringFast(string s)
        {
            //Indent();
            this._output.Append('\"');
            this._output.Append(s);
            this._output.Append('\"');
        }

        private void WriteString(string s)
        {
            //Indent();
            this._output.Append('\"');
            int runIndex = -1;
            for (int index = 0; index < s.Length; ++index)
            {
                char c = s[index];
                if (c >= ' ' && c < 128 && c != '\"' && c != '\\')
                {
                    if (runIndex == -1)
                    {
                        runIndex = index;
                    }
                    continue;
                }
                if (runIndex != -1)
                {
                    this._output.Append(s, runIndex, index - runIndex);
                    runIndex = -1;
                }
                switch (c)
                {
                    case '\t':
                        this._output.Append("\\t");
                        break;
                    case '\r':
                        this._output.Append("\\r");
                        break;
                    case '\n':
                        this._output.Append("\\n");
                        break;
                    case '"':
                    case '\\':
                        this._output.Append('\\');
                        this._output.Append(c);
                        break;
                    default:
                        this._output.Append("\\u");
                        this._output.Append(((int) c).ToString("X4", NumberFormatInfo.InvariantInfo));
                        break;
                }
            }
            if (runIndex != -1)
            {
                this._output.Append(s, runIndex, s.Length - runIndex);
            }
            this._output.Append('\"');
        }
    }
}